package com.ease.util {

   import com.ease.util.events.DataEvent;
   import com.ease.util.events.MethodReturnEvent;
   import com.ease.util.logging.LogManager;

   import flash.utils.Dictionary;
   import flash.utils.getQualifiedClassName;
   import flash.xml.XMLDocument;
   import flash.xml.XMLNode;

   import mx.logging.ILogger;
   import mx.rpc.xml.SimpleXMLEncoder;

   /**
    * A general purpose serializer to convert actionscript object
    * into its MXML representation. For the reverse process use
    * <code>MxmlDeserializer</code> class. Note that
    * the MXML deserialization requires all class types, in the
    * xml document, to be referenced in the containing application.
    *
    * @example The following example demonstrate the use of MxmlSerializer:
    *
    * <listing version="3.0">
    *    var serializer:MxmlSerializer = new MxmlSerializer();
    *    var deserializer:MxmlDeserializer = new MxmlDeserializer(classLoader);
    *    //...
    *    // Marshalling actionscript object.
    *    var objectXml:XML = serializer.objectToXml(object);
    *    // ...
    *    // Unmarshalling mxml document to actionscript object.
    *    var object:Object = deserializer.xmlToObject(objectXml);
    *    // ...
    * </listing>
    *
    * If the <code>strict</code> property is false, then this class supports
    * a slight variation of MXML where properties class tags are optional.
    * If a property class tag is not specified, then the object's property
    * class (using reflection) is used.
    * For example, given:
    *
    * <listing version="3.0">
    *    public class PurchaseOrder {
    *       public var amount:Number;
    *       public var address:Address;
    *    }
    *    public class Address {
    *       public var street:String;
    *       public var city:String;
    *    }
    * </listing>
    * A PurchaseOrder could be written as:
    * <pre>
    * &lt; com.test.PurchaseOrder amount="12.99" &gt;
    *    &lt; address city="Palo Alto" /&gt;
    * &lt;/ com.test.PurchaseOrder &gt;
    * </pre>
    *
    * <p/> If nothing is specified, all properties of object instance will be
    * serialized into XML. It is desirable to restrict the number of properties
    * with the help of the [Persistable] tag.
    *
    * <p/> Once you mark some properties with [Persistable] tag, only these properties
    * will be serialized and all others will be ignored.
    *
    * <p/> Among all properties scheduled for serialization, you can specify order
    * in which those properties are to be serialized. This is done via the
    * [ProcessingOrder] tag.
    *
    * <p/> [ProcessingOrder] is a general purpose tag which
    * takes two arguments: <code>tag</code> and <code>order</code>. For the purpose of
    * serialization, the <code>tag</code> argument need to have value
    * <code>Persistable</code> and <code>order</code> can be any integer.
    *
    * <p/> Here is an example:
    * <listing version="3.0">
    * public class ContactDetails {
    *    [Persistable]
    *    [ProcessingOrder(tag="Persistable", order="20")]
    *    public var name:String;
    *
    *    [Persistable]
    *    [ProcessingOrder(tag="Persistable", order="10")]
    *    // "surname" will be serialized before, "name" because it has smaller
    *    // order = 10 compared to "name" which has order = 20.
    *    public var surname:String;
    *
    *    // This one will be skipped, since it has no [Persistable] tag, but
    *    // other properties do.
    *    public var nickname:String;
    *
    *    [Persistable]
    *    // This one has a default order of 0, so it will be serialized before the
    *    // other properties. Generally it is the same as specifying
    *    // [ProcessingOrder(tag="Persistable", order="0"]);
    *    public var phone:String;
    * }
    * </listing>
    *
    * <p/> As a side effect, when a property is serialized first, then it will be
    * deserialized first when the resulting XML is passed to
    * <code>MxmlDeserializer</code> instance.
    *
    * <p/>If you do not have [Persistable] tag on the property names, then the class is
    * treated as if all properties are persistable, therefore the following example is
    * also valid:
    *
    * <listing version="3.0">
    * public class ContactDetails {
    *    [ProcessingOrder(tag="Persistable", order="20")]
    *    public var name:String;
    *
    *    [ProcessingOrder(tag="Persistable", order="10")]
    *    // "surname" will be serialized before, "name" because it has smaller
    *    // order = 10 compared to "name" which has order = 20.
    *    public var surname:String;
    * }
    * </listing>
    *
    * <p/> Since the default order for [Persistable] properties is <code>0</code> you can
    * specify negative value is you want some  properties to be persisted before the
    * rest of the properties, e.g.:
    *
    * <listing version="3.0">
    * public class ContactDetails {
    *    public var name:String;
    *
    *    [ProcessingOrder(tag="Persistable", order="-10")]
    *    // All properties will be serialized, but this one will be
    *    // first because other do have default order = 0, and this one has order = -10.
    *    // There is no guarantee between the properties with same order (e.g. order = 0)
    *    //  which one will serialized first and which one next.
    *    public var surname:String;
    *
    *    public var nickname:String;
    *
    *    public var phone:String;
    * }
    * </listing>
    *
    * @see MxmlDeserializer
    */
   public class MxmlSerializer {

      private static var _logger:ILogger = LogManager.getLogger(MxmlSerializer);

      /**
       * Number of objects found so far, during serialize process
       */
      private var _refCounter:uint;

      /**
       * The actual object references found so far, during serialize.
       * Map an object to id.
       */
      private var _referenceIdByObject:Dictionary;

      /**
       * The metadata used for serialization and deserialization.
       */
      private var _metadata:MxmlMetadata = new MxmlMetadata;

      /**
       * Initialize in instance of this class
       */
      public function MxmlSerializer(loader:IClassLoader = null):void {
         _metadata.classLoader = loader;
         // This will initialize the internal references array.
         clear();
      }

      /**
       * The class loader instance used to retrieve type information when serializing
       * objects.
       */
      public function get classLoader():IClassLoader {
         return _metadata.classLoader;
      }

      /**
       * Converts the given source object into its MXML representation.
       *
       * @param source
       *    object to be converted into MXML document.
       * @param references
       *    A <code>Dictionary</code> where on each object corresponds a
       *    unique <code>string</code> or <code>uint</code>
       *    value treated as the object id.
       *    This dictionary is merged into the internal dictionary of serialized objects.
       *    When an object is serialized the dictionary is searched and if object
       *    reference is found there, the object is not fully serialized,
       *    but is referenced by its id.
       *    If you do not supply this parameter, the duplicate references
       *    will be tracked internally. This internal references dictionary
       *    will be accumulated until you explicitly call
       *    <code>clear</code> method.
       *    <strong>It is mandatory that if you supply the <code>references</code>
       *    parameter, the ids it holds be non numeric (but can be alpha numeric)
       *    cause numeric ids are used internally and collision may occur</strong>
       * @return
       *    the MXML representation of the passed in source object.
       * @throws Error
       *    Any error happening during serialization is thrown as <code>Error</code>
       *    object.
       * @see #clear()
       */
      public function objectToXml(source:Object, references:Dictionary = null):XML {
         // Initialize the references dictionary
         initializeReferences(references);

         var threadContext:ThreadContext = new ThreadContext();
         threadContext.addWorkItem(new WorkItem(source));

         var thread:PseudoThread = new PseudoThread(
            0, 1, objectToXmlInternal, threadContext, "objectToXml");

         var onThreadTerminated:Function = function (event:DataEvent):void {
            thread.removeEventListener(
               PseudoThread.THREAD_TERMINATED_EVENT, arguments.callee);
            var c:ThreadContext = event.data as ThreadContext;
            writeMetadata(c);
         }

         thread.addEventListener(PseudoThread.THREAD_TERMINATED_EVENT, onThreadTerminated);

         // Start the thread this frame.
         thread.startNow();

         // At this point thread execution is complete.
         if (threadContext.error != null) {
            // This is sync method => report errors via exception.
            throw threadContext.error;
         }

         // Upon completion the result will be in the treadContext
         return threadContext.result;
      }

      /**
       * An asynchronous version of the <code>objectToXml</code> method.
       * @param callback
       *    A function to call when the work is completed. Need to take one parameter
       *    of type <code>MethodReturnEvent</code>
       * Example:
       * <listing version="3.0">
       * public function onObjectToXmlCompleted(event:MethodReturnEvent):void {
       *    ...
       * }
       * </listing>
       * @param callContext
       *    Callback data to pass to the <code>callback</code> when calling it. Will be
       *    returned in the <code>MethodReturnEvent.callContext</code> property.
       * @see objectToXml
       */
      public function objectToXmlAsync(source:Object, references:Dictionary = null,
                                       callback:Function = null,
                                       callContext:Object = null):void {
         // Initialize the references dictionary
         initializeReferences(references);

         var threadContext:ThreadContext = new ThreadContext(callback, callContext);
         threadContext.addWorkItem(new WorkItem(source));

         var thread:PseudoThread = new PseudoThread(
            20, 1, objectToXmlInternal, threadContext, "objectToXmlAsync");

         var onThreadTerminated:Function = function (event:DataEvent):void {
            thread.removeEventListener(
               PseudoThread.THREAD_TERMINATED_EVENT, arguments.callee);
            var c:ThreadContext = event.data as ThreadContext;
            writeMetadata(c);
            c.callback(new MethodReturnEvent(c.result, c.error, c.callContext));
         }

         thread.addEventListener(PseudoThread.THREAD_TERMINATED_EVENT, onThreadTerminated);

         // Start the thread in the next frame.
         thread.start();
      }

      /**
       * The actual work-horse method which does the magic.
       */
      private function objectToXmlInternal(context:ThreadContext):Boolean {
         if (context.nextWorkItem() == null) {
            // No more work.
            return true;
         }

         // Get the object to work on.
         var source:Object = context.workItem.source;

         // Construct an XML node which corresponds the the current object.
         var xml:XML = <Object/>;

         var className:String = getQualifiedClassName(source);
         className = className.replace("::",".");
         xml.setName(className);

         if (MxmlUtil.isSimpleType(className)) {
            xml = processSimpleTypes(source, className);
            context.setResult(xml);
            return false;
         }

         if (addReferenceAttribute(source, xml)) {
            // If we already have encountered this object, we return.
            context.setResult(xml);
            return false;
         }

         if (MxmlUtil.isContainerType(className)) {
            context.setResult(xml);
            processContainers(source, xml, context);
            return false;
         }

         // We have created XML node for the source object. We set it as result of the
         // the current work item.
         context.setResult(xml);

         // Below we handle only properties defined in the type description of the object.
         processProperties(source, xml, context);

         // there is still work that needs to be done.
         return false;
      }

      /**
       * A helper method to help serialize object properties discoverable from the
       * object's type definition.
       */
      private function processProperties(source:Object, sourceXml:XML,
                                         context:ThreadContext):void {
         var properties:Array = getProperties(source);
         for each(var propName:String in properties) {
            try {
               var propValue:Object = source[propName];
               var propXml:XML = getPropertyXml(propName, propValue, true, context);
               sourceXml.appendChild(propXml);
            } catch (error:Error) {
               _logger.error(error.message);
            }
         }
      }

      /**
       * A helper method to serialize a simple type.
       */
      private function processSimpleTypes(source:Object, className:String):XML {
         var xmlDoc:XMLDocument = new XMLDocument();
         var encoder:SimpleXMLEncoder = new SimpleXMLEncoder(xmlDoc);
         var xmlNode:XMLNode = encoder.encodeValue(source, new QName(className), xmlDoc);
         var retVal:XML = XML(xmlNode);
         if (source is XML) {
            // If the simple type is already XML, the encoder above creates
            // only the root XML node, so we need to add the rest of the data manually.
            retVal.appendChild(source)
         }
         return retVal;
      }

      /**
       * Takes care of serializing container objects properly.
       */
      private function processContainers(source:Object, sourceXml:XML,
                                         context:ThreadContext):void {
         if (source is Array) {
            processArray(source as Array, sourceXml, context);
         } else if (source is Dictionary) {
            processDictionary(source as Dictionary, sourceXml, context);
         } else {
            processObjectDictionary(source, sourceXml, context);
         }
      }

      /**
       * A helper method to serialize arrays.
       */
      private function processArray(source:Array, sourceXml:XML,
                                    context:ThreadContext):void {
         var array:Array = source as Array;
         for each(var item:Object in array) {
            context.addWorkItem(new WorkItem(item, sourceXml));
         }
      }

      /**
       * A helper method to serialize new style dictionary
       */
      private function processDictionary(source:Dictionary, sourceXml:XML,
                                         context:ThreadContext):void {
         for (var key:* in source) {
            var value:Object = source[key];
            var keyValue:XML = <keyAndValue></keyAndValue>;
            sourceXml.appendChild(keyValue);
            // Push the key for processing
            context.addWorkItem(new WorkItem(key, keyValue));
            if (value != null) {
               // And the value after that
               context.addWorkItem(new WorkItem(value, keyValue));
            }
         }
      }

      /**
       * A helper method to serialize dictionary based on the Object class
       * (old style dictionary)
       */
      private function processObjectDictionary(source:Object, sourceXml:XML,
                                               context:ThreadContext):void {
         for (var key:String in source) {
            var value:Object = source[key];
            var propXml:XML = getPropertyXml(key, value, false /*isTypeProperty*/, context);
            sourceXml.appendChild(propXml);
         }
      }

      /**
       * Converts the property name/value pair into an XML node.
       *
       * @param propertyName
       *    The property name represented as an XML tag after
       *    processing.
       * @param propertyValue
       *    The property value contained within the property name tag
       *    after processing.
       * @param isTypeProperty
       *    Whether or not the property denoted by the <code>propertyName</code>
       *    is declared in the type definition. If it is not, we need to save the
       *    property type. If it is, then there is not need, so in certain cases
       *    we can skip the type information for simple types of properties.
       *    Note that we respect this parameter only for simple properties. For
       *    more complex properties, because of inheritance, the type of the property
       *    from the property definition is not enough guarantee that we have not stored
       *    an object from derived class.
       * @return
       *    Name/value pair as an XML node.
       */
      private function getPropertyXml(propertyName:String, propertyValue:Object,
                                      isTypeProperty:Boolean,
                                      context:ThreadContext):XML {
         var xmlProperty:XML=<new/>
         xmlProperty.setName(MxmlUtil.escape(propertyName));

         var type:String = getQualifiedClassName(propertyValue);

         if (propertyValue == null) {
            xmlProperty.appendChild(MxmlUtil.NULL);
         } else if (isTypeProperty && MxmlUtil.isSimpleType(type)){
            // Store without type info. No complex processing, so append to the
            // xml immediately.
            xmlProperty.appendChild(propertyValue);
         } else {
            // Store with type info. May be complex. Create a work item.
            context.addWorkItem(new WorkItem(propertyValue, xmlProperty));
         }

         return xmlProperty;
      }

      [ArrayElementType("String")]
      /**
       * Returns the list of properties found in the type definition of the object.
       * @param object
       *    The object for which you need the property names of the serializable
       *    properties.
       */
      private function getProperties(object:Object):Array {
         var props:Array = MxmlUtil.getProperties(
            object, MxmlUtil.PROPS_GUI | MxmlUtil.PROPS_PERSISTABLE);

         if (props.length <= 0) {
            props = MxmlUtil.getProperties(object, MxmlUtil.PROPS_ALL);
         }

         return props;
      }

      /**
       * Helper to correctly initialize the serializer internal reference dictionary.
       */
      private function initializeReferences(incomingReferences:Dictionary):void {
         if (incomingReferences != null) {
            MixedUtil.copyDictionary(incomingReferences, _referenceIdByObject);
         }
      }

      /**
       * Set a reference id to the <code>XML</code> representation of the
       * <code>source</code> object. If this is the first occurrence of <code>source</code>
       * instead of reference id, just an id is set.
       *
       * @return <code>true</code> if the object is already referenced/serialized
       *    before. Also the <strong>ATTRIB_REF</strong> attribute is set to the
       *    id of the object.  If this is the first time an object is met,
       *    then the <strong>ATTRIB_ID</strong> attributed is set instead of
       *    the <strong>ATTRIB_REF</strong> attribute.
       * @see MxmlBase.ATTRIB_ID MmlBase.ATTRIB_REF
       */
      private function addReferenceAttribute(source:Object, xmlObject:XML):Boolean {
         var retVal:Boolean;

         var objId:Object = _referenceIdByObject[source];
         if (objId != null) {
            xmlObject.@[MxmlUtil.ATTRIB_REF] = objId;
            retVal = true;
         } else {
            xmlObject.@[MxmlUtil.ATTRIB_ID] = _refCounter;
            _referenceIdByObject[source] = _refCounter;
            _refCounter++;
            retVal = false;
         }

         var ref:int = findLoadInfoRef(source);
         if (ref != -1) {
            xmlObject.@[MxmlUtil.ATTRIB_INFO_REF] = ref;
         }

         return retVal;
      }

      /**
       * Returns the object loading information id for the given object.  If the
       * loading info is already cached, it will return the existing id, or else
       * it will create a new reference id.
       */
      private function findLoadInfoRef(source:Object):int {
         var info:Object = _metadata.classLoader.getClassLoadInfo(source);
         if (info == null) {
            // Object load info not available for this source
            return -1;
         }

         return _metadata.insertOrUpdateLoadInfo(info);
      }

      /**
       * Write the metadata for the object that is getting serialized.  The metadata
       * includes the class loader and object loading information.
       */
      private function writeMetadata(context:ThreadContext):void {
         if (context.result == null) {
            // No metadata for the serialized object
            return;
         }

         // Writing the metadata
         _metadata.save(context.result);
      }

      /**
       * Clears any state this instance may be maintaining, e.g. IDs of the
       * objects serialized so far.
       * @internal
       * Clear the internal map where for each serialized object an id is kept.
       * This map helps tracking multiple references to each object and
       * serialize the object just once, instead each time it is referenced.
       * The map exists during the lifetime of the <code>MxmlSerializer</code>
       * object. You may want to clear it between different calls to
       * <code>objectToXml</code> if you keep the instance around.
       *
       * Keeping the internal reference around is useful if you serialize a
       * few objects in a row with references to each other or common
       * references to third objects.
       */
      public function clear():void {
         _referenceIdByObject = new Dictionary();
         _refCounter = 0;
      }

   } // class
} // package

class ThreadContext {
   public var callback:Function;
   public var callContext:Object;
   public var result:XML;

   [InjectableError]
   public var error:Error;

   [ArrayElementType("WorkItem")]
   private var _workItems:Array = [];
   private var _wi:WorkItem = null;

   public function ThreadContext(callback:Function = null, callContext:Object = null) {
      this.callback = callback;
      this.callContext = callContext;
   }

   /**
    * Advanced to the next work item.
    */
   public function nextWorkItem():WorkItem {
      _wi = _workItems.length > 0 ? _workItems.shift() : null;
      return _wi;
   }

   /**
    * Add a work item for processing.
    */
   public function addWorkItem(wi:WorkItem):void {
      _workItems.push(wi);
   }

   /**
    * Return the current work item.
    */
   public function get workItem():WorkItem {
      return _wi;
   }

   /**
    * Set the result of the current work item processing.
    */
   public function setResult(value:XML):void {
      if ((_wi == null) || !_wi.setResult(value)) {
         this.result = value;
      }
   }
}

class WorkItem {
   public var source:Object = null;
   public var xmlHost:XML = null;

   /**
    * @param source
    *    The object that needs to be converted into XML
    * @param xmlHost
    *    After conversion, this is where to add the new xml node as a child.
    */
   public function WorkItem(source:Object, xmlHost:XML = null) {
      this.source  = source;
      this.xmlHost = xmlHost;
   }

   /**
    * Set the result of the work item. In our case appends the xml node that comes
    * as <code>value</code> to the right parent xml node.
    */
   public function setResult(value:XML):Boolean {
      var retVal:Boolean = false;
      if (xmlHost != null) {
         xmlHost.appendChild(value);
         retVal = true;
      }
      return retVal;
   }
}